今天要介紹的是 Filter
,這是我非常喜歡的功能,Filter 有點像管道 Middleware 的延伸,會在管道結束後執行,Filter 的作用是,可在 Action 執行前
和執行後
對 Request 進行加工處理,我們可以把一些通用的程式邏輯抽離,例如驗證、例外處理和修改回傳內容等等,在透過 Attribute 掛載到 Action 上,如此做可使 Action 更專注於本身的工作 關注點分離
,獨立的 Filter 模組更易於抽換和擴充,提高程式的內聚力
和降低耦合度
,讓程式更好維護。
而在 Webform 時代我們如果想達到類似的功能,可以使用傳統的 ASP.NET 管道模型,也就是 Middleware 的前身,在 ASP.NET 的管道中,Request 會經過多個 Module 最後才會抵達 Handler,Handler 就是我們比較常寫的 ASPX 和 ASHX,Handler 結束後會再通過原來的 Module 原路返回,因此我們可以利用 Module 來對 Request 進行加工處理,不過沒有辦法像 Filter 一樣靈活,這篇因為重點在 Filter 所以就不對 Module 多做介紹。
接下來要進入本篇的重點,在開發 API 時我們希望所有的 API 能有一致的輸出格式,這樣使用者就不需要為每個 API 去做不同的接收方式,讓 API 使用起來更方便,Filter 可以分為下面三類,後面我會利用這三種不同的 Filter 來統一 API 的輸出格式。
AuthorizationFilter
: 在所有 Filter 之前執行,用於驗證 Request 是否合法。ActionFilter
: 裡面有兩個方法分別對應 Action 執行前和執行後。ExceptionFilter
: 會在發生異常時執行。Filter 流程圖:
上圖藍色箭頭為 Request 經過 Filter 的正常流程,首先會經過 AuthorizationFilter,驗證使用者資訊是否合法,接著通過 ActionFilter,最後到達 Action,結束後在經過 ActionFilter 返回,AuthorizationFilter 只有進來時才會執行,而橘色箭頭是,如果在 ActionFilter 或 Action 內程式出現異常 Exception,那麼就會被 ExceptionFilter 攔截做異常的處理,這裡一定有人會問如果在 AuthorizationFilter 內出現異常呢,沒錯這裡是需要注意的地方,我們不應該在 AuthorizationFilter 拋出異常,因為它不會被 ExceptionFilter 攔截處理。
新增 ResultViewModel 類別,作為我們所有 API 的統一介面,
類別內有下面三個屬性:
success
: 代表請求是否執行成功。msg
: 存放異常的錯誤訊息。data
: 存放請求成功後回傳的資料。程式碼:
namespace ViewModel
{
public class ResultViewModel
{
public bool success { get; set; }
public string msg { get; set; }
public object data { get; set; }
}
}
新增 ResultAttribute 類別,繼承 ActionFilter 覆寫 OnActionExecuted 方法,該 Filter 的作用是包裝 Action 回傳的資料,將資料放入 ResultViewModel 的 data 屬性內,再回傳出去,這個 Filter 可以搭配 IgnoreResult Attribute 使用,如果我們希望有些 Controller 或 Action 的回傳資料不要經過包裝處理,例如檔案下載,那麼可以掛上 IgnoreResult 就會忽略這些 Action。
程式碼:
namespace Filters
{
public class ResultAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
if (actionExecutedContext.Exception != null)
{
return;
}
var ignoreResult1 = actionExecutedContext.ActionContext.ActionDescriptor.GetCustomAttributes<IgnoreResultAttribute>().FirstOrDefault();
var ignoreResult2 = actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<IgnoreResultAttribute>().FirstOrDefault();
if (ignoreResult1 != null || ignoreResult2 != null)
{
return;
}
var objectContent = actionExecutedContext.Response.Content as ObjectContent;
var data = objectContent?.Value;
var result = new ResultViewModel
{
success = true,
data = data
};
actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse(result);
}
}
}
程式碼:
namespace Filters
{
public class IgnoreResultAttribute : Attribute
{
}
}
新增 CustomException 類別,有時候如果我們不希望回傳的錯誤訊息,包含了敏感的資訊,例如執行SQL語法出錯時,會回傳部分的SQL語句,可能包含了欄位資訊或資料等等,因此就可以利用 CustomException 來識別這個 Exception 是不是已經被我們處理過,可否傳到外部去。
程式碼:
namespace Exceptions
{
public class CustomException : Exception
{
public CustomException(string message) : base(message)
{
}
}
}
新增 ExceptionAttribute 類別,繼承 ExceptionFilter,該 Filter 會攔截異常,將錯誤訊息填入 ResultViewModel 的 msg 屬性內,然後將包裝後的異常回傳,不過這裡我並沒有判斷 Exception 是否是 CustomException,因為我還是習慣將所有異常訊息都回傳,這個可以視專案需求調整。
程式碼:
namespace Filters
{
public class ExceptionAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
var result = new ResultViewModel
{
success = false,
msg = actionExecutedContext.Exception.Message
};
actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse(result);
}
}
}
新增 CustomAuthorizeAttribute 類別,繼承 AuthorizationFilter,功能很單純,僅用來判斷使用者是否有登入,這裡有使用到 上一篇 實作的 UserManager,而更細的身分判斷我通常會寫在 Action 內,Filter 只做第一層最簡單的防護,CustomAuthorize 還可以搭配原 Web API 內建的 AllowAnonymous Attribute 使用,掛上這個 Attribute 的 Controller 或 Action 將不會執行驗證權限的動作,代表這是個公開的 API,任何人都可以存取,Filter 內我有用 try catch 包住所有程式,就如同上面提到的,我們不應該在 AuthorizationFilter 內拋出異常,因為它不會被 ExceptionFilter 捕捉。
程式碼:
namespace Filters
{
public class CustomAuthorizeAttribute : AuthorizationFilterAttribute
{
protected readonly UserManager _userManager;
public CustomAuthorizeAttribute()
{
_userManager = new UserManager();
}
public override void OnAuthorization(HttpActionContext actionContext)
{
try
{
if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0)
{
return;
}
if (actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0)
{
return;
}
var user = _userManager.GetCurrentUser();
if (user == null)
{
throw new CustomException("沒有權限。");
}
}
catch (Exception ex)
{
if (!(ex is CustomException))
{
ex = new CustomException("權限驗證異常。");
}
var result = new ResultViewModel
{
success = false,
msg = ex.Message
};
actionContext.Response = actionContext.Request.CreateResponse(result);
}
}
}
}
我會新增一個 BaseController 並在 Controller 掛上 CustomAuthorize,然後讓其它 Controller 繼承,這樣就可以讓所有的 API 受到最基本的權限驗證保護,而公開的 API 再加上 AllowAnonymous Attribute 關閉驗證,這樣做有個好處,可以防止未來新增 Action,或由別人來維護程式時忘記加上權限驗證,導致資料外洩,接著新增一個 TestFilterController 並掛上 Exception 和 Result 來測試一下各個 Filter 的功能。
程式碼:
namespace Api
{
[CustomAuthorize]
public class BaseController : ApiController
{
public BaseController()
{
}
}
}
namespace Api
{
[Result]
[Exception]
[RoutePrefix("api/testFilter")]
public class TestFilterController : BaseController
{
//正常
[HttpGet]
[Route("getStudents_1")]
public List<Student> GetStudents_1()
{
return CreateStudents();
}
//忽略權限驗證
[AllowAnonymous]
[HttpGet]
[Route("getStudents_2")]
public List<Student> GetStudents_2()
{
return CreateStudents();
}
//忽略 ResultFilter
[IgnoreResult]
[HttpGet]
[Route("getStudents_3")]
public List<Student> GetStudents_3()
{
return CreateStudents();
}
//拋出異常
[HttpGet]
[Route("getStudents_4")]
public List<Student> GetStudents_4()
{
throw new CustomException("取得資料失敗");
}
private List<Student> CreateStudents()
{
return new List<Student>
{
new Student
{
Id = 100,
Name = "小明"
},
new Student
{
Id = 101,
Name = "小華"
},
};
}
}
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
}
資料夾結構
在未登入
狀態下對四個 API 的測試結果:
正常: api/testFilter/getStudents_1
忽略權限驗證: api/testFilter/getStudents_2
忽略 ResultFilter: api/testFilter/getStudents_3
拋出異常: api/testFilter/getStudents_4
在登入
狀態下對四個 API 的測試結果:
正常: api/testFilter/getStudents_1
忽略權限驗證: api/testFilter/getStudents_2
忽略 ResultFilter: api/testFilter/getStudents_3
拋出異常: api/testFilter/getStudents_4
這篇我們使用 Filter 來統一 API 的回傳格式和例外處理,除了方便使用者使用外,程式邏輯的抽離讓 Action 能更專注於自己的工作,可自由組合的 Filter,讓 API 面對各種不同需求時有更大的彈性,且將結果的處理拉到 Action 之外,才不會破壞原來的寫法,依舊可由 Action 的回傳型態看出 API 回傳的資料結構,不會因為要統一格式,就造成 Action 都回傳 ResultViewModel,程式的可讀性變差。
今天就介紹到這裡,感謝大家觀看。
使用Asp.Net MVC打造Web Api (16) - 統一輸入/出格式以及異常處理策略
使用Asp.Net MVC打造Web Api (20) - 整合AOP功能
[鐵人賽 Day14] ASP.NET Core 2 系列 - Filters
我今天弄MVC專案也想寫個自訂filter
結果System.Web.Http.Filters
找不到在哪
記得API專案沒這問題...
後來查到答案是Nuget安裝Microsoft.AspNet.WebApi.SelfHost
..
如果是 MVC 的 Filter 好像不用另裝 Nuget
您裝的 Microsoft.AspNet.WebApi.SelfHost
是 Web API 1 的 Filter
這篇是使用 Web API 2 會需要這幾個
如果是 .NET Core 則不分 MVC 和 Web API 整合在一起了
真的是好多版本啊,哈哈哈
fysh711426
結果這是web api 1
用的啊
怎麼覺得.Net framework套件非常混亂啊...
以為filter是基本功能
.NET Core感覺整裡的比較好的感覺
.NET Core 的 Web API 好用很多,不習慣 1 和 2 的 Request 只能讀取一次,不過 .NET Core 還不成熟,不敢用在專案上。
public class CustomAuthorizeAttribute : AuthorizationFilterAttribute
{
protected readonly UserManager _userManager;
public CustomAuthorizeAttribute()
{
_userManager = new UserManager();
}
}
在編釋時,出現錯誤,錯誤訊息為嚴重性 程式碼 說明 專案 檔案 行 隱藏項目狀態
錯誤 CS0305 使用泛型 類型 'UserManager' 時需要 1 個類型引數 WebAPITest D:\05-TEST\WebAPITest\WebAPITest\Filter\CustomAuthorizeAttribute.cs 18 作用中
請問要如何解決,我是用2019
using Exceptions;
using Microsoft.AspNet.Identity;
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using ViewModel;
namespace Filters
{
public class CustomAuthorizeAttribute : AuthorizationFilterAttribute
{
protected readonly UserManager _userManager;
public CustomAuthorizeAttribute()
{
_userManager = new UserManager();
}
public override void OnAuthorization(HttpActionContext actionContext)
{
try
{
if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0)
{
return;
}
if (actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0)
{
return;
}
var user = _userManager.GetCurrentUser();
if (user == null)
{
throw new CustomException("沒有權限。");
}
}
catch (Exception ex)
{
if (!(ex is CustomException))
{
ex = new CustomException("權限驗證異常。");
}
var result = new ResultViewModel
{
success = false,
msg = ex.Message
};
actionContext.Response = actionContext.Request.CreateResponse(result);
}
}
}
}
這裡的 UserManager 是
[C#][ASP.NET] Web API 開發心得 (4) - 使用 FormsAuthentication 進行 API 授權驗證
這篇的 AuthManager,我忘記改名稱了
protected readonly AuthManager _authManager;
public CustomAuthorizeAttribute()
{
_authManager = new AuthManager();
}
...
大大不好意思想請教
var objectContent = actionExecutedContext.Response.Content as ObjectContent;
var data = objectContent?.Value;
針對這兩行
有點不懂as ObjectContent的意思
與下面的?.的運算是什麼用意
這兩個運算有特別的稱呼嗎@@?
還有objectContent不太懂是用來做什麼的物件
as ObjectContent
是轉型的意思actionExecutedContext.Response.Content
內可以放多種型態的物件,其型態取決於我們 Action 內回傳的內容
private List<Student> CreateStudents()
{
// Student 會被包裝成 ObjectContent
return new Student
{
Id = 100,
Name = "小明"
};
}
而 ResultAttribute 目標是處理一般物件的回傳,所以將其型態轉為 ObjectContent 進行後續處理
那其他的型態還有 StreamContent、StringContent、等等,這些就不是 ResultAttribute 要處理的內容
所以這裡又設計了 IgnoreResult,指定這個 Attribute 後,程式會略過 ResultAttribute,使用原來的 HttpContent 處理 (StreamContent、StringContent)
不過現在看來這種設計不是很好,耦合度太大了
?.
是 C# 6.0 的功能 Null條件運算子
以 var data = objectContent?.Value
為例,如果 objectContent 為 null,程式會停在此處,並回傳 null,並不會繼續執行後面的 .Value,類似於下列程式碼
var data = null;
if (objectContent != null)
data = objectContent.Value
謝謝大大光速回應!! 我大致上了解了
不過還是有點不了解objectContent
目前只知道大概是Object 因為是基礎物件所以能夠裝各種東西
不知道是否是這個用意所以才會轉型成objectContent
恩,類似
Response.Content
的型態是 HttpContent 可以裝
ObjectContent、StreamContent、StringContent 等子類別物件
public class ObjectContent : HttpContent
{
...
}
因為我想要使用 ObjectContent 物件,所以才將參考型態從父類別 HttpContent,轉為子類別 ObjectContent
這部分可以搜尋物件導向的 多型
概念